Passed
Pull Request — master (#136)
by
unknown
02:11
created

Frame.ts ➔ decompressBuffer   B

Complexity

Conditions 6

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 32
rs 8.4666
c 0
b 0
f 0
cc 6
1
import zlib = require('zlib')
2
import {
3
    Flags,
4
    getHeaderSize,
5
    FrameHeader
6
} from './FrameHeader'
7
import * as Frames from './Frames'
8
import * as ID3Util from './ID3Util'
9
import { isKeyOf } from "./util"
10
11
type HeaderInfo = {
12
    identifier: string
13
    headerSize: number
14
    bodySize: number
15
    flags: Flags
16
}
17
18
export class Frame {
19
    identifier: string
20
    private value: unknown
21
    flags: Flags
22
23
    constructor(identifier: string, value: unknown, flags: Flags = {}) {
24
        this.identifier = identifier
25
        this.value = value
26
        this.flags = flags
27
    }
28
29
    static createFromBuffer = createFromBuffer
30
31
    makeBuffer() {
32
        return makeFrameBuffer(this.identifier, this.value)
33
    }
34
35
    getValue() {
36
        return this.value
37
    }
38
}
39
40
function createFromBuffer(
41
    frameBuffer: Buffer,
42
    version: number
43
): Frame | null {
44
    const headerSize = getHeaderSize(version)
45
    // Specification requirement
46
    if (frameBuffer.length < headerSize + 1) {
47
        return null
48
    }
49
    const headerBuffer = frameBuffer.subarray(0, headerSize)
50
    const header: HeaderInfo = {
51
        headerSize,
52
        ...FrameHeader.createFromBuffer(headerBuffer, version)
53
    }
54
    if (header.flags.encryption) {
55
        return null
56
    }
57
58
    const body = decompressBody(
59
        header.flags,
60
        getDataLength(header, frameBuffer),
61
        getBody(header, frameBuffer)
62
    )
63
    if (body) {
64
        const value = makeFrameValue(header.identifier, body, version)
65
        if (value) {
66
            return new Frame(header.identifier, value, header.flags)
67
        }
68
    }
69
    return null
70
}
71
72
function makeFrameBuffer(identifier: string, value: unknown) {
73
    if (isKeyOf(identifier, Frames.Frames)) {
74
        return Frames.Frames[identifier].create(value)
75
    }
76
    if (identifier.startsWith('T')) {
77
        return Frames.GENERIC_TEXT.create(identifier, value)
78
    }
79
    if (identifier.startsWith('W')) {
80
        return Frames.GENERIC_URL.create(identifier, value)
81
    }
82
    return null
83
}
84
85
function makeFrameValue(identifier:string, body: Buffer, version: number) {
86
    if (isKeyOf(identifier, Frames.Frames)) {
87
        return Frames.Frames[identifier].read(body, version)
88
    }
89
    if (identifier.startsWith('T')) {
90
        return Frames.GENERIC_TEXT.read(body)
91
    }
92
    if (identifier.startsWith('W')) {
93
        return Frames.GENERIC_URL.read(body)
94
    }
95
    return null
96
}
97
98
function getBody({flags, headerSize, bodySize}: HeaderInfo, buffer: Buffer) {
99
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
100
    const bodyStart = headerSize + bodyOffset
101
    const bodyEnd = bodyStart + bodySize - bodyOffset
102
    const body = buffer.subarray(bodyStart, bodyEnd)
103
    if (flags.unsynchronisation) {
104
        // This method should stay in ID3Util for now because it's also used in the Tag's header which we don't have a class for.
105
        return ID3Util.processUnsynchronisedBuffer(body)
106
    }
107
    return body
108
}
109
110
function getDataLength({flags, headerSize}: HeaderInfo, buffer: Buffer) {
111
    return flags.dataLengthIndicator ? buffer.readInt32BE(headerSize) : 0
112
}
113
114
function decompressBody(
115
    {compression}: Flags,
116
    dataLength: number,
117
    body: Buffer
118
) {
119
    return compression ? decompressBuffer(body, dataLength) : body
120
}
121
122
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
123
    if (buffer.length < 5) {
124
        return null
125
    }
126
127
    // ID3 spec defines that compression is stored in ZLIB format,
128
    // but doesn't specify if header is present or not.
129
    // ZLIB has a 2-byte header.
130
    // 1. try if header + body decompression
131
    // 2. else try if header is not stored (assume that all content is deflated "body")
132
    // 3. else try if inflation works if the header is omitted (implementation dependent)
133
    const tryDecompress = () => {
134
        try {
135
            return zlib.inflateSync(buffer)
136
        } catch (error) {
137
            try {
138
                return zlib.inflateRawSync(buffer)
139
            } catch (error) {
140
                try {
141
                    return zlib.inflateRawSync(buffer.subarray(2))
142
                } catch (error) {
143
                    return null
144
                }
145
            }
146
        }
147
    }
148
    const decompressed = tryDecompress()
149
    if (decompressed && decompressed.length === expectedDecompressedLength) {
150
        return decompressed
151
    }
152
    return null
153
}
154